import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  EventEmitter,
  forwardRef,
  Input,
  Output,
  ViewEncapsulation,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DateService } from '../services/api';
import { LvConfig } from '../config';
import { LvDateMode, LvDateRangeTag } from '../types';
import { TypeUtils } from '../utils/type';
import { StandardDate } from './standard-date';

const CALENDAR_OPTIONS = {
  firstDayOfWeek: 1,
  ROWS: 6,
  COLS: 7,
};

@Component({
  selector: 'lv-calendar, [lv-calendar]',
  exportAs: 'lvCalendar',
  template: '',
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef((): typeof CalendarBaseComponent => CalendarBaseComponent),
      multi: true,
    },
  ],
})
export class CalendarBaseComponent implements ControlValueAccessor {
  @Input() lvIsRange: boolean; // for date range picker

  @Input() lvPickerMode: LvDateMode;

  @Input() lvHoverValue: StandardDate; // for date range picker

  @Input() lvCurrentInput: LvDateRangeTag; // for date range picker

  @Input() lvSelectedValue: StandardDate[]; // for date range picker

  @Input() lvActiveDate: StandardDate = null;

  @Input() lvDisabledDate: (date: Date) => boolean;

  @Output() readonly lvActiveDateChange = new EventEmitter<any>();
  @Output() readonly valueChange = new EventEmitter<StandardDate>();

  @Output() readonly lvCellMouseEnter = new EventEmitter<StandardDate>();

  @Output() readonly lvCellMouseLeave = new EventEmitter<StandardDate>();

  C_rows = [];

  C_years = [];

  C_months = [];
  C_10years = [];
  C_weekDays = [];
  typeUtils = TypeUtils;
  activeDate: StandardDate;
  language = LvConfig.language;
  prefixCls: string = 'lv-calendar';

  protected _value: StandardDate;
  constructor(
    protected cdr: ChangeDetectorRef,
    protected dateService: DateService,
  ) {}

  get value(): StandardDate {
    return this._value;
  }

  @Input()
  set value(date: StandardDate) {
    if (this.typeUtils.isNil(date)) {
      !this.lvActiveDate && (this.activeDate = new StandardDate(new Date()));
      this._value = null;
    } else {
      !this.lvActiveDate && (this.activeDate = date);
      this._value = date;
    }
  }

  _onTouch: any = (): void => {};

  _onChange: any = (): void => {};

  registerOnChange(fn: any): void {
    this._onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this._onTouch = fn;
  }

  // eslint-disable-next-line
  writeValue(value: boolean): void {
    this.cdr.markForCheck();
  }

  getCell(date: StandardDate): any {
    return {
      value: date.localDate,
      label: this.dateService.format(date.localDate, 'dd'),
      isSelected: false,
      isDisabled: false,
      isToday: false,
      isInRange: false,
      isRangeStart: false,
      isRangeEnd: false,
      isInHoverRange: false,
      isHoverRangeStart: false,
      isHoverRangeEnd: false,
      isLastMonth: false,
      isNextMonth: false,
      title: this.dateService.format(date.localDate, 'longDate'),
      content: `${date.getDate()}`,
      mouseenter: (): void => this.lvCellMouseEnter.emit(date),
      classMap: null,
    };
  }

  isDateChange(change): boolean {
    if (change) {
      const preValue: StandardDate | StandardDate[] = change.previousValue;
      const curValue: StandardDate | StandardDate[] = change.currentValue;
      if (Array.isArray(curValue)) {
        return (
          !Array.isArray(preValue) ||
          curValue.length !== preValue.length ||
          curValue.some((value, index): boolean => !preValue[index] || !preValue[index].isSameDay(value))
        );
      } else {
        return !((!preValue && !curValue) || (preValue && curValue && (preValue as StandardDate).isSameDay(curValue)));
      }
    }
    return false;
  }

  getWeekDays(): any[] {
    const weekDays = [];
    const start = this.activeDate.calendarStart(this.firstDayOfWeekConfig());
    for (let colIndex = 0; colIndex < CALENDAR_OPTIONS.COLS; colIndex++) {
      const day = new StandardDate(start.addDays(colIndex));
      weekDays[colIndex] = {
        full: this.dateService.format(day.localDate, 'EEEE'), // eg. Tuesday
        short: this.dateService.format(day.localDate, 'E'), // eg. Tue
        veryShort: this.dateService.format(day.localDate, 'EEEEEE'), // eg. Tu
        single: this.dateService.format(day.localDate, 'EEEEE'), // eg. T
      };
    }
    return weekDays;
  }

  getCalendarRows(): any[] {
    const rows = [];
    const firstDayOfMoth = this.activeDate.calendarStart(this.firstDayOfWeekConfig());

    for (let rowIndex = 0; rowIndex < CALENDAR_OPTIONS.ROWS; rowIndex++) {
      const firstDayOfWeek = new StandardDate(firstDayOfMoth.addDays(rowIndex * 7));
      let row = {
        isActive: false, // 当月的日期
        isCurrent: false, // 今天所在行
        cells: [],
        year: firstDayOfWeek.getYear(),
        isBlank: false,
      };

      row = this.setRowData(row, rowIndex, firstDayOfMoth);

      row.isBlank = row.cells.length === row.cells.filter((item): any => item.isNextMonth || item.isLastMonth).length;
      rows.push(row);
    }

    return rows;
  }

  setCellClassMap(cell): void {
    cell.classMap = {
      [`${this.prefixCls}-today`]: cell.isToday,
      [`${this.prefixCls}-last-month-day`]: cell.isLastMonth,
      [`${this.prefixCls}-next-month-day`]: cell.isNextMonth,
      [`${this.prefixCls}-selected-day`]: cell.isSelected,
      [`${this.prefixCls}-disabled-day`]: cell.isDisabled,
      [`${this.prefixCls}-range-start-day`]: cell.isRangeStart,
      [`${this.prefixCls}-range-end-day`]: cell.isRangeEnd,
      [`${this.prefixCls}-range-day`]: cell.isInRange,
      [`${this.prefixCls}-hover-range-day`]: cell.isInHoverRange,
      [`${this.prefixCls}-hover-range-start-day`]: cell.isHoverRangeStart,
      [`${this.prefixCls}-hover-range-end-day`]: cell.isHoverRangeEnd,
    };
  }

  setRowData(row, rowIndex: number, firstDayOfMoth: StandardDate): any {
    for (let colIndex = 0; colIndex < CALENDAR_OPTIONS.COLS; colIndex++) {
      const date = new StandardDate(firstDayOfMoth.addDays(rowIndex * 7 + colIndex));
      const cell = this.getCell(date);

      if (this.lvDisabledDate?.(date.localDate)) {
        cell.isDisabled = true;
      }

      if (date.isToday()) {
        cell.isToday = true;
        row.isCurrent = true;
      }

      if (date.isCompareMonth(this.activeDate, 'less-than')) {
        cell.isLastMonth = true;
      }

      if (date.isCompareMonth(this.activeDate, 'more-than')) {
        cell.isNextMonth = true;
      }

      this.changeCellBaySelectedValue(date, cell, row);

      this.setCellClassMap(cell);

      row.cells.push(cell);
    }
    return row;
  }

  getCalendarMonths(): any[] {
    const months = [];

    for (let mIndex = 0; mIndex < 12; mIndex++) {
      const numYear = this.activeDate.getYear();
      const baseDate = new StandardDate(new Date('1970-1-1'));
      const date = new StandardDate(new StandardDate(baseDate.setYear(numYear)).setMonth(mIndex));

      const month = {
        value: date.localDate,
        label: this.dateService.format(date.localDate, 'MMMM'),
        isSelected: false,
        isDisabled: false,
        content: `${this.dateService.format(date.localDate, this.language === 'en-us' ? 'MMMM' : 'MMM')}`,
        classMap: null,
      };

      if (this.value && date.isSameMonth(this.value)) {
        month.isSelected = true;
      }

      month.isDisabled = this.isAllMonthDayDisabled(date.localDate, this.lvPickerMode === 'month');

      month.classMap = {
        [`${this.prefixCls}-selected-cell`]: month.isSelected,
        [`${this.prefixCls}-disabled-cell`]: month.isDisabled,
      };

      months.push(month);
    }

    return months;
  }

  setDisabledStartAndEnd(
    cells: Array<{
      value: Date;
      isDisabled: boolean;
      isDisabledStart: boolean;
      isDisabledEnd: boolean;
      classMap: { [key in string]: boolean };
    }>,
    subtractFunc: (value: Date, diff: number) => Date,
    addFunc: (value: Date, diff: number) => Date,
    disabledDate: (value: Date) => boolean,
  ): void {
    cells.forEach((cell, index): void => {
      if (!cell.isDisabled) {
        cell.isDisabledStart = false;
        cell.isDisabledEnd = false;
      } else {
        const preCell = cells[index - 1];
        if (!preCell) {
          // 当前cell为当前面板的第一个cell
          const preCellDay = subtractFunc(cell.value, 1);
          cell.isDisabledStart = !disabledDate(preCellDay);
        } else {
          cell.isDisabledStart = !preCell.isDisabled;
        }

        const nextCell = cells[index + 1];
        if (!nextCell) {
          // 当前cell为当前面板的最后一个cell
          const nextCellDay = addFunc(cell.value, 1);
          cell.isDisabledEnd = !disabledDate(nextCellDay);
        } else {
          cell.isDisabledEnd = !nextCell.isDisabled;
        }
      }
      cell.classMap = {
        ...cell.classMap,
        [`${this.prefixCls}-disabled-start-cell`]: cell.isDisabledStart,
        [`${this.prefixCls}-disabled-end-cell`]: cell.isDisabledEnd,
      };
    });
  }

  getCalendarYears(): any[] {
    const years = [];

    for (let yIndex = 0; yIndex < 12; yIndex++) {
      const firstYear = this.activeDate.getYear() - (this.activeDate.getYear() % 10) - 1;
      const currentMonth = this.activeDate.getMonth();
      const baseDate = new StandardDate(new Date('1970-1-1'));
      const date = new StandardDate(new StandardDate(baseDate.setYear(firstYear + yIndex)).setMonth(currentMonth));
      const year = {
        value: date.localDate,
        label: this.dateService.format(date.localDate, 'yyyy'),
        isSelected: false,
        isDisabled: false,
        isLast10Years: false,
        isNext10Years: false,
        content: `${this.dateService.format(date.localDate, 'yyyy')}`,
        classMap: null,
      };

      if (this.value && date.isSameYear(this.value)) {
        year.isSelected = true;
      }

      year.isDisabled = this.isAllYearDayDisabled(date.localDate, this.lvPickerMode === 'year');

      if (yIndex === 0) {
        year.isLast10Years = true;
      }

      if (yIndex === 11) {
        year.isNext10Years = true;
      }

      year.classMap = {
        [`${this.prefixCls}-last-year`]: year.isLast10Years,
        [`${this.prefixCls}-next-year`]: year.isNext10Years,
        [`${this.prefixCls}-selected-cell`]: year.isSelected,
        [`${this.prefixCls}-disabled-cell`]: year.isDisabled,
      };

      years.push(year);
    }

    return years;
  }

  getCalendar10Years(): any[] {
    const years = [];

    for (let yIndex = 0; yIndex < 12; yIndex++) {
      const firstYear = this.activeDate.getYear() - (this.activeDate.getYear() % 100) - 10;
      const currentMonth = this.activeDate.getMonth();
      const baseDate = new StandardDate(new Date('1970-1-1'));
      const dateStart = new StandardDate(
        new StandardDate(baseDate.setYear(firstYear + yIndex * 10)).setMonth(currentMonth),
      );
      const dateEnd = new StandardDate(
        new StandardDate(baseDate.setYear(firstYear + yIndex * 10 + 9)).setMonth(currentMonth),
      );
      const year = {
        value: dateStart.localDate,
        label: this.dateService.format(dateStart.localDate, 'yyyy'),
        isSelected: false,
        isLast100Years: false,
        isNext100Years: false,
        content: `${this.dateService.format(dateStart.localDate, 'yyyy')}-${this.dateService.format(dateEnd.localDate, 'yyyy')}`,
        classMap: null,
      };

      if (this.value) {
        const currentFirstYear = new StandardDate(baseDate.setYear(this.value.getYear() - (this.value.getYear() % 10)));
        if (dateStart.isSameYear(currentFirstYear)) {
          year.isSelected = true;
        }
      }

      if (yIndex === 0) {
        year.isLast100Years = true;
      }

      if (yIndex === 11) {
        year.isNext100Years = true;
      }

      year.classMap = {
        [`${this.prefixCls}-last-year`]: year.isLast100Years,
        [`${this.prefixCls}-next-year`]: year.isNext100Years,
        [`${this.prefixCls}-selected-cell`]: year.isSelected,
      };

      years.push(year);
    }

    return years;
  }

  renderCalendar(
    calendarMap: {
      showMonthPanel?: boolean;
      showDatePanel?: boolean;
      showYearPanel?: boolean;
      show10YearsPanel?: boolean;
    } = { showMonthPanel: true, showDatePanel: true, showYearPanel: true, show10YearsPanel: true },
  ): void {
    if (calendarMap.showDatePanel) {
      this.C_weekDays = this.getWeekDays();
      this.C_rows = this.getCalendarRows();
      const rows = [];
      this.C_rows.forEach((row): void => {
        rows.push(...row.cells.filter((item): boolean => !item.isLastMonth && !item.isNextMonth));
      });
      this.setDisabledStartAndEnd(
        rows,
        (value: Date, diff: number): Date => new StandardDate(value).addDays(-diff),
        (value: Date, diff: number): Date => new StandardDate(value).addDays(diff),
        (value: Date): boolean => this.lvDisabledDate?.(value),
      );
    }
    if (calendarMap.showYearPanel) {
      this.C_years = this.getCalendarYears();
      this.setDisabledStartAndEnd(
        this.C_years.filter((item): boolean => !item.isLast10Years && !item.isNext10Years),
        (value: Date, diff: number): Date => new StandardDate(value).addYears(-diff),
        (value: Date, diff: number): Date => new StandardDate(value).addYears(diff),
        (value: Date): boolean => this.isAllYearDayDisabled(value, this.lvPickerMode === 'year'),
      );
    }
    if (calendarMap.show10YearsPanel) {
      this.C_10years = this.getCalendar10Years();
    }
    if (calendarMap.showMonthPanel) {
      this.C_months = this.getCalendarMonths();
      this.setDisabledStartAndEnd(
        this.C_months,
        (value: Date, diff: number): Date => new StandardDate(value).addMonths(-diff),
        (value: Date, diff: number): Date => new StandardDate(value).addMonths(diff),
        (value: Date): boolean => this.isAllMonthDayDisabled(value, this.lvPickerMode === 'month'),
      );
    }
  }

  private firstDayOfWeekConfig(): number {
    return LvConfig.calendarOptions.firstDayOfWeek === undefined
      ? CALENDAR_OPTIONS.firstDayOfWeek
      : LvConfig.calendarOptions.firstDayOfWeek;
  }

  private isAllYearDayDisabled(date: Date, independent = false): boolean {
    if (independent) {
      return this.lvDisabledDate?.(date);
    }
    const currentYear = date.getFullYear();
    for (let i = 0; i < 12; i++) {
      const month = new Date(currentYear, i, 1);
      if (!this.isAllMonthDayDisabled(month, this.lvPickerMode === 'month')) {
        return false;
      }
    }
    return true;
  }

  private isAllMonthDayDisabled(date: Date, independent = false): boolean {
    if (independent) {
      return this.lvDisabledDate?.(date);
    }
    const newDate = new Date(date.getFullYear(), date.getMonth(), 1);
    while (newDate.getMonth() === date.getMonth()) {
      if (!this.lvDisabledDate?.(newDate)) {
        return false;
      }
      newDate.setDate(newDate.getDate() + 1);
    }
    return true;
  }

  private changeCellBaySelectedValue(
    date: StandardDate,
    cell: { [key: string]: any },
    row: { isCurrent: boolean; cells: any[]; year: number; isBlank: boolean; isActive: boolean },
  ): void {
    if (Array.isArray(this.lvSelectedValue) && this.lvSelectedValue.length > 0) {
      const start = this.lvSelectedValue[0];
      const end = this.lvSelectedValue[1];
      // 日期范围开始
      if (start && date.isSameDay(start)) {
        cell.isSelected = true;
        cell.isRangeStart = true;
      }
      // 日期范围开始
      if (end && date.isSameDay(end)) {
        cell.isSelected = true;
        cell.isRangeEnd = true;
      }
      // 日期范围区间
      const flag = start && end && date.isCompareDay(start, 'after') && date.isCompareDay(end, 'before');
      if (flag) {
        cell.isInRange = true;
      }
      // 鼠标hover时的范围
      this.changeCellByHoverValue(end, date, cell, start);
    } else if (this.value && date.isSameDay(this.value)) {
      cell.isSelected = true;
      row.isActive = true;
    }
  }

  private changeCellByHoverValue(
    end: StandardDate,
    date: StandardDate,
    cell: { [key: string]: any },
    start: StandardDate,
  ): void {
    if (!this.lvHoverValue) {
      return;
    }
    if (this.lvCurrentInput === 'start') {
      if (end && date.isCompareDay(end, 'before') && date.isCompareDay(this.lvHoverValue, 'after')) {
        cell.isInHoverRange = true;
      }
      if (end && date.isSameDay(end)) {
        if (this.lvHoverValue.isCompareDay(end, 'before')) {
          cell.isHoverRangeEnd = true;
        }
      }
      if (date.isSameDay(this.lvHoverValue)) {
        cell.isHoverRangeStart = true;
      }
    }
    if (this.lvCurrentInput === 'end') {
      const flag = start && date.isCompareDay(start, 'after') && date.isCompareDay(this.lvHoverValue, 'before');
      if (flag) {
        cell.isInHoverRange = true;
      }
      if (start && date.isSameDay(start)) {
        if (this.lvHoverValue.isCompareDay(start, 'after')) {
          cell.isHoverRangeStart = true;
        }
      }
      if (date.isSameDay(this.lvHoverValue)) {
        cell.isHoverRangeEnd = true;
      }
    }
  }
}
